package cn.com.bluemoon.daps.model.service.impl;

import cn.com.bluemoon.daps.common.domain.BaseModel;
import cn.com.bluemoon.daps.common.domain.ResultBean;
import cn.com.bluemoon.daps.common.domain.UserInfoHolder;
import cn.com.bluemoon.daps.common.enums.BmStatus;
import cn.com.bluemoon.daps.common.enums.DatabaseType;
import cn.com.bluemoon.daps.common.exception.DapException;
import cn.com.bluemoon.daps.common.exception.DapThrowException;
import cn.com.bluemoon.daps.common.toolkit.BmAssetUtils;
import cn.com.bluemoon.daps.common.toolkit.DbTypeHelper;
import cn.com.bluemoon.daps.common.toolkit.WebDownloadUtils;
import cn.com.bluemoon.daps.model.core.cache.ModelCacheHelper;
import cn.com.bluemoon.daps.model.core.file.IMetaData;
import cn.com.bluemoon.daps.model.core.file.TableInfoDto;
import cn.com.bluemoon.daps.model.core.file.entry.MultiEntryImpl;
import cn.com.bluemoon.daps.model.core.file.parser.AbstractFileParser;
import cn.com.bluemoon.daps.model.core.file.parser.FileParserFactory;
import cn.com.bluemoon.daps.model.core.file.parser.FileType;
import cn.com.bluemoon.daps.model.core.file.report.ImportBaseInfo;
import cn.com.bluemoon.daps.model.core.file.report.ImportReport;
import cn.com.bluemoon.daps.model.core.file.transformer.ImportEntryTableDto;
import cn.com.bluemoon.daps.model.core.file.transformer.ToDaoDataTransformer;
import cn.com.bluemoon.daps.model.core.file.validate.DataRelationCheck;
import cn.com.bluemoon.daps.model.core.upload.DapModelExcelUpload;
import cn.com.bluemoon.daps.model.core.upload.DapModelImportExeclDataListenerV2;
import cn.com.bluemoon.daps.model.core.upload.TableInfo;
import cn.com.bluemoon.daps.model.entity.DapDataModel;
import cn.com.bluemoon.daps.model.entity.DapDataModelGroup;
import cn.com.bluemoon.daps.model.entity.DapDataModelTable;
import cn.com.bluemoon.daps.model.entity.DapDataModelTableField;
import cn.com.bluemoon.daps.model.service.DapDataModelGroupService;
import cn.com.bluemoon.daps.model.service.DapDataModelImportService;
import cn.com.bluemoon.daps.model.service.DapDataModelService;
import cn.com.bluemoon.daps.standard.api.RemoteSystemCategoryService;
import cn.com.bluemoon.daps.standard.vo.SysTopicCategorySimpleVo;
import cn.com.bluemoon.daps.system.entity.DapSystemDatasource;
import cn.hutool.core.util.NumberUtil;
import com.alibaba.druid.DbType;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.google.common.collect.ImmutableMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import reactor.util.function.Tuple2;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 模型导入服务
 *
 * @author Jarod.Kong
 */
@Service
@Slf4j
public class DapDataModelImportServiceImpl implements DapDataModelImportService {
    private static final Pattern DATA_TYPE_COMPILE = Pattern.compile("^([a-z]+)\\((\\d+)(,(\\d+))?\\)$");
    @Autowired
    private DapDataModelService modelService;
    @Autowired
    private DapSystemModuleFacade systemModuleFacade;
    @Autowired
    private MultiEntryImpl multiEntry;
    @Autowired
    private RemoteSystemCategoryService remoteSystemCategoryService;
    @Autowired
    private DapModelExcelUpload dapModelExcelUpload;
    @Autowired
    private DapDataModelGroupService groupService;

    /**
     * 导入
     * excel/sql
     * 模型id
     *
     * @param mFile   excel/sql...
     * @param modelId 模型id
     */
    @SuppressWarnings("rawtypes")
    @Override
    public ImportReport fileImport(MultipartFile mFile, String modelId) {
        Path tempFile = null;
        final ImportReport.ImportReportBuilder reportBuilder = ImportReport.builder()
                .filename(mFile.getOriginalFilename())
                .uploader(UserInfoHolder.getAccount());
        final StopWatch sw = new StopWatch();
        try {
            sw.start("导入数据预准备");
            tempFile = Files.createTempFile(Paths.get(System.getProperty("user.dir")), "model_db_", "_" + mFile.getOriginalFilename());
            File file = tempFile.toFile();
            mFile.transferTo(file);
            DapDataModel dataModel = modelService.getByIdAsset(modelId);
            String dataSourceId = dataModel.getDataSourceId();
            Optional<DapSystemDatasource> datasource = systemModuleFacade.getSysDataSourceById(dataSourceId);
            BmAssetUtils.isTrue(datasource.isPresent(), "模型无法获取数据源");
            DatabaseType databaseType = datasource.get().getDatabaseType();
            DbType dbType = DbTypeHelper.getDruidDbType(databaseType);
            log.info("导入文件{},数据库类型:{}", tempFile, dbType);
            sw.stop();
            sw.start("解析文件");
            ResultBean<List<TableInfoDto>> parser = FileParserFactory.parser(file, FileType.EXCEL, dbType);
            sw.stop();
            if (parser.isError()) {
                @SuppressWarnings("unchecked") List<ImportBaseInfo> ImportBaseInfos = (List<ImportBaseInfo>) parser.getMoreData().get(AbstractFileParser.PARSER_VALiD_ERROR_KEY);
                return reportBuilder.result(false).errorInfos(ImportBaseInfos).msg("解析文件内容错误").build();
            }
            List<TableInfoDto> tableInfoDtos = parser.getContent();
            sw.start("检查解析结果");
            ResultBean<List<IMetaData>> check = DataRelationCheck.check(tableInfoDtos, dataModel, databaseType);
            if (check.isError()) {
                @SuppressWarnings("unchecked") List<ImportBaseInfo> ImportBaseInfos = (List<ImportBaseInfo>) check.getMoreData().get(AbstractFileParser.PARSER_VALiD_ERROR_KEY);
                return reportBuilder.result(false).errorInfos(ImportBaseInfos).msg("检查解析结果错误").build();
            }
            sw.stop();
            sw.start("转换结果集");
            ToDaoDataTransformer transformer = new ToDaoDataTransformer(dataModel);
            List<ImportEntryTableDto> entryModelList = transformer.transform(tableInfoDtos);
            sw.stop();
            sw.start("结果集入库");
//                ResultBean<Boolean> entryResult = resultEntry.entry(entryModelList);
            ResultBean<Boolean> entryResult = multiEntry.mutilEntry(entryModelList);
            sw.stop();
            if (entryResult.isError()) {
                return reportBuilder.result(false).errorInfos(null).msg("数据集入库失败").build();
            }
        } catch (IOException e) {
            log.error("解析文件失败！！", e);
        } catch (ClassCastException e) {
            log.error("错误数据强制类型异常，检查[FileParserFactory.parser]&[DataRelationCheck.check]" +
                    "{}的错误结果类型是否为List<ImportBaseInfo>", System.lineSeparator(), e);
        } catch (ExcelAnalysisException e) {
            log.error("导入文件异常！！", e);
            Throwable cause = e.getCause();
            if (cause instanceof DapException) {
                DapException exception = (DapException) cause;
                throw new DapException(exception.getMessage());
            } else {
                throw e;
            }
        } finally {
            log.info("情况导入中的缓存数据");
            ModelCacheHelper.clearCache();
            delTempFile(tempFile);
            log.info("导入完成报告:" + sw.prettyPrint());
            log.info("耗时：{}m", sw.getTotalTimeSeconds() / 60);
        }
        return reportBuilder.result(true).msg("文件导入完成，cost:" + sw.getTotalTimeSeconds() + "s").build();
    }

    /**
     * 下载模板，根据模型id
     */
    @Override
    public void downloadTemplateByModelId(String modelId, HttpServletResponse response) {
        Tuple2<DapDataModel, Optional<DatabaseType>> dbTypeOpt = modelService.findModelAndDbTypeByModelId(modelId);
        DatabaseType dbType = dbTypeOpt.getT2().orElseThrow(() -> new DapThrowException("模型对应的数据源非数据库类型，可能为文件系统数据源!!"));
        log.info("模型id={}下载模板文件数据库类型{}", modelId, dbType);
        downloadTemplateByDbType(response, dbType);
    }

    /**
     * 根据数据类型下模板
     *
     * @param response resp
     * @param dbType   db
     */
    @Override
    public void downloadTemplateByDbType(HttpServletResponse response, DatabaseType dbType) {
        BmAssetUtils.notNull(dbType, "模型对应的数据源的数据库类型不可为空！！");
        String filePath = null;
        switch (dbType) {
            case MYSQL:
                filePath = "download/系统外模型导入模板_MySQL.xlsx";
                break;
            case PGSQL:
                filePath = "download/系统外模型导入模板_数仓模型_PG.xlsx";
                break;
            case HIVE:
                filePath = "download/系统外模型导入模板_数仓模型_Hive.xlsx";
                break;
            default:
                throw new DapThrowException("目前不支持改数据库类型");
        }
        WebDownloadUtils.commonDownload(response, filePath, null);
    }

    @Override
    public void downloadTemplateV2ByModelId(String modelId, HttpServletResponse response) {
        Tuple2<DapDataModel, Optional<DatabaseType>> dbTypeOpt = modelService.findModelAndDbTypeByModelId(modelId);
        DatabaseType dbType = dbTypeOpt.getT2().orElseThrow(() -> new DapThrowException("模型对应的数据源非数据库类型，可能为文件系统数据源!!"));
        log.info("模型id={}下载模板文件数据库类型{}", modelId, dbType);
        downloadTemplateV2ByDbType(response, dbType);
    }

    private String getTemplatePath(DatabaseType dbType) {
        BmAssetUtils.notNull(dbType, "模型对应的数据源的数据库类型不可为空！！");
        String filePath = null;
        switch (dbType) {
            case PGSQL:
                filePath = "download/PG模型表导入模板V1.1.xlsx";
                break;
            case MYSQL:
            case HIVE:
            default:
                throw new DapThrowException("目前不支持改数据库类型");
        }
        return filePath;
    }

    @Override
    public void downloadTemplateV2ByDbType(HttpServletResponse response, DatabaseType dbType) {
        String filePath = getTemplatePath(dbType);
        // 模板加工
        String tempFilePath = null;
        try {
            tempFilePath = createTempFile(filePath);
            WebDownloadUtils.commonDownload(response, tempFilePath, new File(filePath).getName());
        } catch (Exception e) {
            log.error("下载异常", e);
            throw new DapException("下载模板异常", e);
        } finally {
            if (tempFilePath != null) {
                File file = new File(tempFilePath);
                if (file.exists()) {
                    file.delete();
                }
            }
        }
    }

    @Override
    public boolean fileImportV2(MultipartFile mFile, String modelId, UserInfoHolder.UserInfo userInfo) {
        DapDataModel dataModel = modelService.getByIdAsset(modelId);
        String dataSourceId = dataModel.getDataSourceId();
        Optional<DapSystemDatasource> datasource = systemModuleFacade.getSysDataSourceById(dataSourceId);
        BmAssetUtils.isTrue(datasource.isPresent(), "模型无法获取数据源");
        DatabaseType databaseType = datasource.get().getDatabaseType();
        if (!DatabaseType.PGSQL.equals(databaseType) && !DatabaseType.POSTGRESQL.equals(databaseType)){
            throw new DapException("该导入功能只支持数据库类型为PG的模型！");
        }
        final StopWatch sw = new StopWatch();
        Path tempFile = null;
        try {

            sw.start("导入数据预准备");
            tempFile = Files.createTempFile(Paths.get(System.getProperty("user.dir")), "model_db_", "_" + mFile.getOriginalFilename());
            File file = tempFile.toFile();
            mFile.transferTo(file);
            sw.stop();

            sw.start("解析文件");
            List<TableInfo> tableInfos = new ArrayList<>();
            EasyExcel.read(file, new DapModelImportExeclDataListenerV2(tableInfos)).headRowNumber(0).sheet(0).doRead();
            if (tableInfos.isEmpty()) {
                return true;
            }
            sw.stop();

            sw.start("表校验");
            List<String> errors = dapModelExcelUpload.doValidate(modelId, tableInfos);
            if (!errors.isEmpty()) {
                throw new DapException(JSON.toJSONString(
                        ImmutableMap.of("errorMsg", "导入失败错误信息，错误数" + errors.size() + "个", "detail", errors)));
            }
            sw.stop();

            sw.start("表信息完善");
            // 根据模型id获取模型第一层分组
            List<DapDataModelGroup> groups = groupService.findByModelIdOrderByRankAndCreateTime(modelId);
            if (groups.isEmpty()) {
                throw new DapException("模型分组为空，暂不支持导入！");
            }
            Map<String, String> grpNameAndId = groups.stream().filter(g -> StringUtils.isEmpty(g.getPid()))
                    .collect(Collectors.toMap(g -> g.getName().toLowerCase(), BaseModel::getId, (a, b) -> a));
            List<ImportEntryTableDto> entryModelList = getImportEntryTableDtos(modelId, dataSourceId, tableInfos, grpNameAndId, userInfo);
            sw.stop();

            sw.start("表入库");
            multiEntry.mutilEntry(entryModelList);
            sw.stop();

        } catch (IOException e) {
            log.error("导入失败", e);
            throw new DapException("导入文件失败", e);
        } finally {
            delTempFile(tempFile);
            log.info("导入完成报告:" + sw.prettyPrint());
            log.info("耗时：{}m", sw.getTotalTimeSeconds() / 60);
        }
        return true;
    }

    private List<ImportEntryTableDto> getImportEntryTableDtos(String modelId, String dataSourceId, List<TableInfo> tableInfos, Map<String, String> groupNameAndId, UserInfoHolder.UserInfo userInfo) {
        List<ImportEntryTableDto> entryModelList = new ArrayList<>();
        for (TableInfo tableInfo : tableInfos) {
            TableInfo.Table tableDto = tableInfo.getTable();
            DapDataModelTable table = getTable(modelId, dataSourceId, groupNameAndId, userInfo, tableDto);
            List<TableInfo.FieldInfo> fieldInfos = tableInfo.getFieldInfos();
            String tableId = table.getId();
            List<DapDataModelTableField> fields = getFields(modelId, tableId, fieldInfos, userInfo);
            ImportEntryTableDto build = ImportEntryTableDto.builder().table(table).fields(fields).indexes(Lists.newArrayList())
                    .partitions(Lists.newArrayList()).build();
            entryModelList.add(build);
        }
        return entryModelList;
    }

    private List<DapDataModelTableField> getFields(String modelId,
                                                   String tableId,
                                                   List<TableInfo.FieldInfo> fieldInfos,
                                                   UserInfoHolder.UserInfo userInfo) {
        List<DapDataModelTableField> fields = new ArrayList<>(fieldInfos.size());
        for (int i = 0; i < fieldInfos.size(); i++) {
            TableInfo.FieldInfo fieldInfo = fieldInfos.get(i);
            DapDataModelTableField f = new DapDataModelTableField();
            f.setAutoIncrDefaultValue(null);
            f.setAutoIncrement(0);
            f.setBusinessDesc(fieldInfo.getDesc().getValue());
            f.setComment(fieldInfo.getComment().getValue());
            f.setDataModelId(modelId);
            String fieldDataType = fieldInfo.getType().getValue().trim().toLowerCase();
            Matcher matcher = DATA_TYPE_COMPILE.matcher(fieldDataType);
            if (matcher.find()) {
                String dataTypeName = matcher.group(1);
                String length = matcher.group(2);
                String scale = matcher.group(4);
                f.setDataType(dataTypeName);
                f.setLength(length);
                if (NumberUtil.isInteger(scale)) {
                    f.setDecimalLength(Integer.valueOf(scale));
                }
            } else {
                f.setDataType(fieldDataType);
                if (fieldDataType.indexOf("(") < fieldDataType.lastIndexOf(")")) {
                    f.setLength(fieldDataType.substring(fieldDataType.indexOf("(") + 1, fieldDataType.lastIndexOf(")")));
                }
                f.setDecimalLength(null);
            }
            f.setDefaultValue(fieldInfo.getDefaultValue().getValue());
            f.setFieldName(fieldInfo.getName().getValue());
            f.setModelTableId(tableId);
            f.setNotNull(fieldInfo.getNullable().getValueOrDefault("").equalsIgnoreCase("no") ?  1 : 0);
            f.setPrimaryKey(fieldInfo.getPrimary().getValueOrDefault("").equalsIgnoreCase("yes") ?  1 : 0);
            f.setFieldRank(i + 1);
            f.setCompoundType(null);
            f.setStandardId("0");
            f.setUnSigned(0);
            f.setZeroFill(0);
            f.setPId("0");
            f.setDataNewest(1);
            f.setStandardMethod(null);
            f.setId(IdWorker.getIdStr());
            f.setCreateBy(userInfo.getAccount());
            f.setCreateTime(LocalDateTime.now());
            f.setBmStatus(BmStatus.ON);

            fields.add(f);
        }
        return fields;
    }

    private DapDataModelTable getTable(String modelId, String dataSourceId,
                                       Map<String, String> grpNameAndId,
                                       UserInfoHolder.UserInfo userInfo,
                                       TableInfo.Table tableDto) {
        DapDataModelTable table = new DapDataModelTable();
        table.setBusinessDesc(tableDto.getDesc().getValue());
        table.setCategoryId(tableDto.getTopicCategory().getVirtualValue());
        table.setDataModelId(modelId);
        table.setDataSourceId(dataSourceId);
        table.setEngName(tableDto.getName().getValue());
        table.setName(tableDto.getComment().getValue());
        String tbLowerCase = tableDto.getName().getValue().trim().toLowerCase();
        table.setTableSchema(tbLowerCase.startsWith("res_") ? "res" : "bluemoon");
        table.setTableCategory(getTableCategoryByTableName(tbLowerCase));
        table.setGroupPid(null);
        String warehouse = dapModelExcelUpload.getTableDataWarehouseByTableName(tbLowerCase);
        table.setGroupId(grpNameAndId.getOrDefault(warehouse, grpNameAndId.keySet().stream().findFirst().orElse(null)));
        table.setWarehouseTb(true);
        table.setRegisterApi(false);
        table.setDataWarehouse(false);
        table.setId(IdWorker.getIdStr());
        table.setCreateBy(userInfo.getAccount());
        table.setCreateTime(LocalDateTime.now());
        table.setBmStatus(BmStatus.ON);
        return table;
    }

    private int getTableCategoryByTableName(String tbLowerCase) {
        int tableCategory = 2;
        if (tbLowerCase.startsWith("dim_")) {
            tableCategory = 1;
        } else if (tbLowerCase.startsWith("o_")) {
            tableCategory = 3;
        }
        return tableCategory;
    }

    private String createTempFile(String filePath) throws IOException {
        String file = Files.createTempFile("temp_model_template_v2", ".xlsx").toFile().getAbsolutePath();
        ExcelWriter excelWriter = EasyExcel.write(file).withTemplate(getInputStream(filePath)).build();
        try {
            // todo hint pg sheet no 1
            WriteSheet writeSheet = EasyExcel.writerSheet(1).build();
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            // list
            excelWriter.fill(getTopicCategory(), fillConfig, writeSheet);
            Map<String, Object> map = new HashMap<>();
            map.put("date", new Date());
            excelWriter.fill(map, writeSheet);
        } finally {
            excelWriter.finish();
        }
        return file;
    }

    private List<SysTopicCategorySimpleVo> getTopicCategory() {
        ResultBean<List<SysTopicCategorySimpleVo>> resultBean = null;
        try {
            resultBean = remoteSystemCategoryService.sysTopicCategories();
        } catch (Exception e) {
            log.error("远程获取主题分类失败", e);
            throw new DapException("远程获取主题分类失败", e);
        }
        List<SysTopicCategorySimpleVo> systemCategoryTreeVos = resultBean.getContent();
        return systemCategoryTreeVos;
    }

    /**
     * 获取文件路径的流
     *
     * @param filePath 文件路径
     * @return 流
     */
    private InputStream getInputStream(String filePath) {
        BmAssetUtils.notNull(filePath, "文件路径不可为空");
        InputStream fis = WebDownloadUtils.class.getClassLoader().getResourceAsStream(filePath);
        if (fis == null) {
            try {
                File file = ResourceUtils.getFile(filePath);
                return new FileInputStream(file);
            } catch (FileNotFoundException e) {
                log.error("获取模板文件失败", e);
                throw new DapThrowException("获取模板文件失败！！");
            }
        } else {
            return fis;
        }
    }

    /**
     * 删除文件
     */
    private void delTempFile(Path tempFile) {
        if (tempFile != null) {
            boolean isDel;
            int trySize = 1;
            try {
                isDel = Files.deleteIfExists(tempFile);
            } catch (IOException e) {
                log.error("尝试-{}，删除临时文件{}失败！！", ++trySize, tempFile, e);
                isDel = tempFile.toFile().delete();
            }
            if (isDel) {
                log.info("尝试-{}，删除上传文件的临时文件{}成功！！", trySize, tempFile);
            }
        }
    }


}
